Análisis de series de tiempo

Author

Tripp Valdez MA

🎓Objetivos

Al final de esta lección serás capaz de:

  • Reconocer los distintos tipos de objetos de fechas y fecha-tiempo.
  • Visualizar series de tiempo.
  • Agrupar datos de acuerdo a lapsos de tiempo.
  • Entender e identificar los patrones principales en las series de tiempo: tendencia, estacional y residual.

Introducción a los objetos de tiempo en R

En R, existen varios tipos de objetos para manejar fechas y horas. Es importante entenderlos antes de comenzar a trabajar con series de tiempo.

  1. Date
  • Representa objetos sin hora YYY-m-d.
  • Se almacena como número de días desde el 1 de enero de 1970.
  1. DateTime
  • Representa objetos con fecha y hora YYY-m-d H:M:S.

  • Podemos encontrarlo en dos formatos distintos:

    • POSIXct almacena la fecha como el número de segundos desde 1970-01-01.
    • POSIXlt almacena la fecha como una lista con componentes individuales (año, mes, día, hroa, etc.)

Veamos algunos ejemplos:

# evaluar la fecha actual
fecha <- Sys.Date()

fecha
[1] "2025-04-23"
class(fecha)
[1] "Date"
#Evaluar la hora actual
hora <- Sys.time()
hora
[1] "2025-04-23 14:25:07 MST"
class(hora)
[1] "POSIXct" "POSIXt" 

Podemos convertir un objeto POSIXct a uno POSIXlt con la función as.POSIXlt

hora_lt <- as.POSIXlt(hora)
hora_lt
[1] "2025-04-23 14:25:07 MST"
class(hora_lt)
[1] "POSIXlt" "POSIXt" 

A simple vista pareciera que no hay cambio, pero la diferencia entre los formatos se hace evidente cuando usamos la función unclass()

unclass(hora)
[1] 1745443508
unclass(hora_lt)
$sec
[1] 7.677949

$min
[1] 25

$hour
[1] 14

$mday
[1] 23

$mon
[1] 3

$year
[1] 125

$wday
[1] 3

$yday
[1] 112

$isdst
[1] 0

$zone
[1] "MST"

$gmtoff
[1] -25200

attr(,"tzone")
[1] ""    "MST" "MDT"
attr(,"balanced")
[1] TRUE

Por engorroso que sea esto, es importante reconocer los distintos formatos de tiempo y fecha ya que en las bases de datos podemos toparnos con distintos formatos y es importante ser capaces de homogenizarlos.

Por ejemplo, veamos la base de datos de

library(tidyverse)

url <- "https://raw.githubusercontent.com/PacktPublishing/Hands-On-Time-Series-Analysis-with-R/master/Chapter02/dates_formats.csv"


dates_df <- read_csv(url)

glimpse(dates_df)
Rows: 22
Columns: 7
$ Japanese_format      <chr> "2017/1/20", "2017/1/21", "2017/1/22", "2017/1/23…
$ US_format            <chr> "1/20/2017", "1/21/2017", "1/22/2017", "1/23/2017…
$ US_long_format       <chr> "Friday, January 20, 2017", "Saturday, January 21…
$ CA_mix_format        <chr> "January 20, 2017", "January 21, 2017", "January …
$ SA_mix_format        <chr> "20 January 2017", "21 January 2017", "22 January…
$ NZ_format            <chr> "20/01/2017", "21/01/2017", "22/01/2017", "23/01/…
$ Excel_Numeric_Format <dbl> 42755, 42756, 42757, 42758, 42759, 42760, 42761, …

Esto es algo que nos podríamos encontrar comúnmente en nuestro trabajo, !Cada fecha tiene un formato completamente diferente!

Para poder homogeneizarlo, necesitamos especificar el formato de cada uno, para lo cual ocupamos símbolos especiales.

Símbolo Significado Ejemplo
%a Abrevia el nombre del día de la semana de acuerdo a la configuración de cada plataforma Sun, Mon, Thu
%A Nombre completo del día de la semana Sunday, Monday, Thursday
%b Nombre abreviado del mes de acuerdo a la configuración de cada plataforma Jan, Feb, Mar
%B Nombre completo del mes January, February, March
%d Día del mes como número decimal 01, 02, 03
%m Mes como número decimal 01, 02, 03
%y Año sin el siglo (dos dígitos) 25
%Y Año con siglo (cuatro dígitos) 2025
dates_df %>% 
  mutate(US_format = as.POSIXct(US_format, format = "%m/%d/%Y"))
# A tibble: 22 × 7
   Japanese_format US_format           US_long_format              CA_mix_format
   <chr>           <dttm>              <chr>                       <chr>        
 1 2017/1/20       2017-01-20 00:00:00 Friday, January 20, 2017    January 20, …
 2 2017/1/21       2017-01-21 00:00:00 Saturday, January 21, 2017  January 21, …
 3 2017/1/22       2017-01-22 00:00:00 Sunday, January 22, 2017    January 22, …
 4 2017/1/23       2017-01-23 00:00:00 Monday, January 23, 2017    January 23, …
 5 2017/1/24       2017-01-24 00:00:00 Tuesday, January 24, 2017   January 24, …
 6 2017/1/25       2017-01-25 00:00:00 Wednesday, January 25, 2017 January 25, …
 7 2017/1/26       2017-01-26 00:00:00 Thursday, January 26, 2017  January 26, …
 8 2017/1/27       2017-01-27 00:00:00 Friday, January 27, 2017    January 27, …
 9 2017/1/28       2017-01-28 00:00:00 Saturday, January 28, 2017  January 28, …
10 2017/1/29       2017-01-29 00:00:00 Sunday, January 29, 2017    January 29, …
# ℹ 12 more rows
# ℹ 3 more variables: SA_mix_format <chr>, NZ_format <chr>,
#   Excel_Numeric_Format <dbl>
¡Cuidado!

Es importante que el formato sea el mismo (uso de / o - como separadores). De lo contrario no se podrá cambiar el formato

Ahora vamos a modificar la columna US_long_format

dates_df %>% 
  mutate(US_long_format = as.POSIXct(US_long_format, format = "%A, %B %d, %Y"))
# A tibble: 22 × 7
   Japanese_format US_format US_long_format      CA_mix_format    SA_mix_format 
   <chr>           <chr>     <dttm>              <chr>            <chr>         
 1 2017/1/20       1/20/2017 2017-01-20 00:00:00 January 20, 2017 20 January 20…
 2 2017/1/21       1/21/2017 2017-01-21 00:00:00 January 21, 2017 21 January 20…
 3 2017/1/22       1/22/2017 2017-01-22 00:00:00 January 22, 2017 22 January 20…
 4 2017/1/23       1/23/2017 2017-01-23 00:00:00 January 23, 2017 23 January 20…
 5 2017/1/24       1/24/2017 2017-01-24 00:00:00 January 24, 2017 24 January 20…
 6 2017/1/25       1/25/2017 2017-01-25 00:00:00 January 25, 2017 25 January 20…
 7 2017/1/26       1/26/2017 2017-01-26 00:00:00 January 26, 2017 26 January 20…
 8 2017/1/27       1/27/2017 2017-01-27 00:00:00 January 27, 2017 27 January 20…
 9 2017/1/28       1/28/2017 2017-01-28 00:00:00 January 28, 2017 28 January 20…
10 2017/1/29       1/29/2017 2017-01-29 00:00:00 January 29, 2017 29 January 20…
# ℹ 12 more rows
# ℹ 2 more variables: NZ_format <chr>, Excel_Numeric_Format <dbl>

Manejo de fechas con Lubridate

Lubridate es un paquete que nos facilita el trabajar con los distintos formatos de fecha y tiempo. Lo único que se requiere hacer es usar el orden correcto de las abreviaciones (y = año; m = mes; d = día).

Para ponerlo a prueba, vamos a hacer las mismas modificaciones que realizamos anteriormente.

Lubridate ya viene incorporado dentro del Tidyverse

dates_df %>% 
  mutate(US_format = mdy(US_format))
# A tibble: 22 × 7
   Japanese_format US_format  US_long_format         CA_mix_format SA_mix_format
   <chr>           <date>     <chr>                  <chr>         <chr>        
 1 2017/1/20       2017-01-20 Friday, January 20, 2… January 20, … 20 January 2…
 2 2017/1/21       2017-01-21 Saturday, January 21,… January 21, … 21 January 2…
 3 2017/1/22       2017-01-22 Sunday, January 22, 2… January 22, … 22 January 2…
 4 2017/1/23       2017-01-23 Monday, January 23, 2… January 23, … 23 January 2…
 5 2017/1/24       2017-01-24 Tuesday, January 24, … January 24, … 24 January 2…
 6 2017/1/25       2017-01-25 Wednesday, January 25… January 25, … 25 January 2…
 7 2017/1/26       2017-01-26 Thursday, January 26,… January 26, … 26 January 2…
 8 2017/1/27       2017-01-27 Friday, January 27, 2… January 27, … 27 January 2…
 9 2017/1/28       2017-01-28 Saturday, January 28,… January 28, … 28 January 2…
10 2017/1/29       2017-01-29 Sunday, January 29, 2… January 29, … 29 January 2…
# ℹ 12 more rows
# ℹ 2 more variables: NZ_format <chr>, Excel_Numeric_Format <dbl>
dates_df %>% 
  mutate(US_long_format = mdy(US_long_format))
# A tibble: 22 × 7
   Japanese_format US_format US_long_format CA_mix_format    SA_mix_format  
   <chr>           <chr>     <date>         <chr>            <chr>          
 1 2017/1/20       1/20/2017 2017-01-20     January 20, 2017 20 January 2017
 2 2017/1/21       1/21/2017 2017-01-21     January 21, 2017 21 January 2017
 3 2017/1/22       1/22/2017 2017-01-22     January 22, 2017 22 January 2017
 4 2017/1/23       1/23/2017 2017-01-23     January 23, 2017 23 January 2017
 5 2017/1/24       1/24/2017 2017-01-24     January 24, 2017 24 January 2017
 6 2017/1/25       1/25/2017 2017-01-25     January 25, 2017 25 January 2017
 7 2017/1/26       1/26/2017 2017-01-26     January 26, 2017 26 January 2017
 8 2017/1/27       1/27/2017 2017-01-27     January 27, 2017 27 January 2017
 9 2017/1/28       1/28/2017 2017-01-28     January 28, 2017 28 January 2017
10 2017/1/29       1/29/2017 2017-01-29     January 29, 2017 29 January 2017
# ℹ 12 more rows
# ℹ 2 more variables: NZ_format <chr>, Excel_Numeric_Format <dbl>

Entonces, dependiendo de la estructura de la fecha, vamos a indicar la función ymd para año, mes, día; mdy para mes, día, año; etc.

Por ejemplo:

dates_df %>% 
  mutate(Japanese_format = ymd(Japanese_format))
# A tibble: 22 × 7
   Japanese_format US_format US_long_format          CA_mix_format SA_mix_format
   <date>          <chr>     <chr>                   <chr>         <chr>        
 1 2017-01-20      1/20/2017 Friday, January 20, 20… January 20, … 20 January 2…
 2 2017-01-21      1/21/2017 Saturday, January 21, … January 21, … 21 January 2…
 3 2017-01-22      1/22/2017 Sunday, January 22, 20… January 22, … 22 January 2…
 4 2017-01-23      1/23/2017 Monday, January 23, 20… January 23, … 23 January 2…
 5 2017-01-24      1/24/2017 Tuesday, January 24, 2… January 24, … 24 January 2…
 6 2017-01-25      1/25/2017 Wednesday, January 25,… January 25, … 25 January 2…
 7 2017-01-26      1/26/2017 Thursday, January 26, … January 26, … 26 January 2…
 8 2017-01-27      1/27/2017 Friday, January 27, 20… January 27, … 27 January 2…
 9 2017-01-28      1/28/2017 Saturday, January 28, … January 28, … 28 January 2…
10 2017-01-29      1/29/2017 Sunday, January 29, 20… January 29, … 29 January 2…
# ℹ 12 more rows
# ℹ 2 more variables: NZ_format <chr>, Excel_Numeric_Format <dbl>
dates_df %>% 
  mutate(SA_mix_format = dmy(SA_mix_format))
# A tibble: 22 × 7
   Japanese_format US_format US_long_format          CA_mix_format SA_mix_format
   <chr>           <chr>     <chr>                   <chr>         <date>       
 1 2017/1/20       1/20/2017 Friday, January 20, 20… January 20, … 2017-01-20   
 2 2017/1/21       1/21/2017 Saturday, January 21, … January 21, … 2017-01-21   
 3 2017/1/22       1/22/2017 Sunday, January 22, 20… January 22, … 2017-01-22   
 4 2017/1/23       1/23/2017 Monday, January 23, 20… January 23, … 2017-01-23   
 5 2017/1/24       1/24/2017 Tuesday, January 24, 2… January 24, … 2017-01-24   
 6 2017/1/25       1/25/2017 Wednesday, January 25,… January 25, … 2017-01-25   
 7 2017/1/26       1/26/2017 Thursday, January 26, … January 26, … 2017-01-26   
 8 2017/1/27       1/27/2017 Friday, January 27, 20… January 27, … 2017-01-27   
 9 2017/1/28       1/28/2017 Saturday, January 28, … January 28, … 2017-01-28   
10 2017/1/29       1/29/2017 Sunday, January 29, 20… January 29, … 2017-01-29   
# ℹ 12 more rows
# ℹ 2 more variables: NZ_format <chr>, Excel_Numeric_Format <dbl>

Ademas, lubridate puede reconocer estructuras mas complejas en las fechas, por ejemplo:

fecha_mx <- "Lunes, 31 de diciembre del 2020"
dmy(fecha_mx)
[1] "2020-12-31"
fecha_us <- "Monday, December 31 2020"
mdy(fecha_us)
[1] "2020-12-31"

Para estructura de fechas mas complejas que incluyen la hora, podemos extender la función agregando _hms.

fecha_us_hora <- "Monday, December 31, 2018 11:59:59 PM"
mdy_hms(fecha_us_hora)
[1] "2018-12-31 23:59:59 UTC"
fecha_mx_hora <- "Lunes, 31 de diciembre, 2018 11:59:59 PM"
dmy_hms(fecha_mx_hora )
[1] "2018-12-31 11:59:59 UTC"
Advertencía

En caso de que haya incomposibilidades en el idioma, quizá sea necesario ajustar el locale. por ejemplo:

Sys.setlocale("LC_TIME", "es_ES.UTF-8")

Extraer componentes de una serie de tiempo

Para analizar mas a detalle como extraer los distintos componentes de una serie de tiempo asi como calcular promedios por intervalos específicos, vamos a utilizar una base de datos de temperatura superficial del mar de dos sitios diferentes y de dos fuentes diferentes.

sst_data <- read_csv("data/OSTIA_SST_LOL-BA_2000-2022.csv")
Rows: 67208 Columns: 4
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr  (2): source, site
dbl  (1): mean_sst
date (1): date

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
Warning

Antes de utilizar esta base de datos, vamos hacer un prefiltrado:

  • Filtrar solamente los datos de Bahia de Los Angeles (BA)
  • Obtener el promedio de cada base de datos (un dato por fecha)
sst_data_tidy <- sst_data %>% 
  filter(site == "BA") %>%
  group_by(date) %>% 
  summarise(temperatura_promedio = mean(mean_sst, na.rm =TRUE)) %>% 
  mutate(date = ymd(date)) 

Antes que nada, podemos graficar la serie de tiempo

ggplot(sst_data_tidy, aes(x = date, y = temperatura_promedio))+
  geom_line()+
  geom_smooth(se = FALSE)+
  labs(x = "Fecha",
       y = "Temperatura")
`geom_smooth()` using method = 'gam' and formula = 'y ~ s(x, bs = "cs")'

Una forma de simplificar una serie de tiempo es agregar los datos por alguna frecuencia especifica (diaria, mensual, estacional, anual)

Para nuestra suerte, lubridate tiene funciones para extraer diferentes componentes de un objeto DateTime

year(sst_data_tidy$temperatura_promedio) %>% 
  head
Warning: tz(): Don't know how to compute timezone for object of class numeric;
returning "UTC".
[1] 1970 1970 1970 1970 1970 1970
month(sst_data_tidy$date, label = TRUE) %>% 
  head
[1] ene ene ene ene ene ene
12 Levels: ene < feb < mar < abr < may < jun < jul < ago < sep < ... < dic

Con esta información, podemos agregar los datos con group_by() para obtener el valor promedio mensual

sst_data_mensual <- sst_data_tidy %>% 
  mutate(año = year(date),
         mes = month(date)) %>% 
  group_by(año, mes) %>% 
  summarise(temperatura_mensual = mean(temperatura_promedio)) %>% 
  ungroup()
`summarise()` has grouped output by 'año'. You can override using the `.groups`
argument.

Esto cumple nuestro cometido, pero observa que las columnas de año y mes son de tipo carácter 🤦‍♂️

Esto lo podemos resolver de dos formar:

  1. Crear una nueva columna donde unimos el año y el mes en un objeto DateTime
sst_data_mensual <- sst_data_mensual %>% 
  mutate(fecha = paste(mes, año, sep = "-")) %>% 
  mutate(fecha = my(fecha))
ggplot(sst_data_mensual, aes(x = fecha, y = temperatura_mensual))+
  geom_line()

  1. Utilizar el paquete Timetk el cual ofrece diversas funciones que facilitan el análisis de series de tiempo.
#install.packages("timetk")
library(timetk)

Entre las funciones tenemos la función summarize_by_time que nos permite agrupar los datos de acuerdo a diversos intervalos de tiempo.

Como promedios mensuales month

sst_data_mensual <- sst_data_tidy %>% 
  summarise_by_time(.date_var = date, # especifica la columna con la fecha
                    .by = "month", #month, daily, year
                    temperatura_mensual = mean(temperatura_promedio))
ggplot(sst_data_mensual, aes(x = date, y = temperatura_mensual))+
  geom_line()

o promedios anuales year

sst_data_anual <- sst_data_tidy %>% 
  summarise_by_time(.date_var = date, 
                    .by = "year", 
                    temperatura_anual = mean(temperatura_promedio))
ggplot(sst_data_anual, aes(x = date, y = temperatura_anual))+
  geom_line()

El paquete timetk también nos permite gráficar directamente nuestra serie de tiempo con la función plot_time-series.

Esta necesita los parámetros:

  • .date_var=: Columna con las fechas en formato DateTime
  • .value=: Valores que se van a gráficar
sst_data_tidy %>% 
  plot_time_series(.date_var = date, .value = temperatura_promedio)

Extraer el componente estacional, tendencia y anomalías

La descomposición de las serie de tiempo busca separar la serie en sus principales componentes: observada, estacional, tendencia y residuo.

  • Observada (Observed): Es la serie de tiempo original.Contiene todas las variaciones, patrones y tendencias presentes en los datos.

  • Estacional (Seasonal): Representa los patrones que se repiten en intervalos fijos (estacionalidad).

  • Tendencia (Trend): Muestra los cambios a largo plazo en la serie y permite identificar aumentos o disminuciones graduales en los datos, eliminando el ruido a corto plazo.

  • Residuo (Remainder): También llamado componente de error o ruido. Es lo que queda después de eliminar la tendencia y la estacionalidad. Idealmente, debe parecerse a un ruido blanco (fluctuaciones aleatorias sin patrón definido).

Estos componentes puedes extraerse con la función plot_stl_diagnostics del paquete timetk.

sst_data_tidy %>% 
  plot_stl_diagnostics(
    .date_var = date,                 # Columna que contiene las fechas de la serie temporal.
    .value = temperatura_promedio,     # Columna con los valores a analizar (temperatura).
    .frequency = "year",               # Estacionalidad de la serie (ciclos de un año).
    .trend = "1 year"                  # Suaviza las fluctuaciones con una ventana de un año.
  )
frequency = 365 observations per 1 year
trend = 366 observations per 1 year

Finalmente podemos obtener boxplot de los datos en diferente estacionalidad (anual, mensual, diaria, etc)

sst_data_tidy %>% 
  plot_seasonal_diagnostics(
    .date_var = date,              
    .value = temperatura_promedio,   
    .feature_set = c("month.lbl", "year")
  )